/**
 *
 */
package com.dalonedrau.jogl.engine;

import java.lang.ref.WeakReference;

import com.dalonedrau.jogl.util.UtilityMethods;
import com.dalonedrau.pool.PooledStringBuilder;
import com.dalonedrau.pool.StringBuilderPool;

/**
 * @author Donald
 */
public final class Engine implements Runnable {
	/** static reference to singleton. */
	private static Engine		engine;
	public static final float DIV_NANO_MILLI = 0.000001f;
	/** nano to milli conversion factor. */
	private static final long	NANO_TO_MILLI	= 1000000L;
	/** nano to second conversion factor. */
	private static final long	NANO_TO_SECOND	= 1000000000L;
	/**
	 * Gets the singleton <code>Engine</code>.
	 * @return <code>Engine</code>
	 */
	public static Engine getEngine() {
		return Engine.engine;
	}
	/** the list of {@link DelayedAction}s. *//*
	private ArrayList<DelayedAction>	delayedActions	=
																new ArrayList<DelayedAction>();
	/**
	 * the list of {@link Entity}s being managed by the <code>Engine</code>/.
	 *//*
	private ArrayList<Entity>			entities		= new ArrayList<Entity>();
	/** the entity id counter. *//*
	private int							entityId		= 0;
	private Entity						found;
	/** the last time garbage was cleared. */
	private long						lastGarbage		= 0L;
	/** <code>LogItem</code> object. */
	// private Log logger = LogFactory.getLog(this.getClass());
	/** the game screen. */
	private volatile GameDrawable		screen;
	/** the game thread. */
	private Thread						thread;
	/*
	private ArrayList<Entity>			toBeRemoved		= new ArrayList<Entity>();
	/**
	 * Creates a new instance of Engine.
	 * @param loop the length in nanoseconds of a game loop
	 */
	public Engine(final long loop) {
		Time.getInstance().setIdealLoopTime(loop);
		Engine.engine = this;
		thread = new Thread(this);
		thread.start();
	}
	/**
	 * Adds a {@link DelayedAction} to be performed.
	 * @param action the {@link DelayedAction}
	 *//*
	public void addDelayedAction(final DelayedAction action) {
		delayedActions.add(action);
	}
	/**
	 * Adds an {@link Entity} to be managed.
	 * @param entity the {@link Entity}
	 *//*
	public void addEntity(final Entity entity) {
		entities.add(entity);
	}
	/** Removes all dead entities from the game. *//*
	private void buryEntities() {
		toBeRemoved.clear();
		int entitiesLen = entities.size();
		for (int i = 0; i < entitiesLen; i++) {
			if (!entities.get(i).isAlive()) {
				toBeRemoved.add(entities.get(i));
			}
		}
		int toBeRemovedLen = toBeRemoved.size();
		for (int outer = 0; outer < toBeRemovedLen; outer++) {
			for (int inner = 0; inner < entitiesLen; inner++) {
				if (entities.get(inner).getID() == toBeRemoved.get(outer)
						.getID()) {
					entities.remove(inner);
					break;
				}
			}
		}
	}
	/**
	 * Updates an {@link Entity}.
	 * @param entity the {@link Entity} being updated
	 * @param time the time in nanoseconds
	 *//*
	private void gameEntityUpdate(final Entity entity, final long time) {
		entity.update(time);
	}*/
	/**
	 * Updates the game.
	 * @param time the time in nanoseconds
	 *//*
	private void gameUpdate(final long time) {
		screen.gameUpdate();
		int len = delayedActions.size();
		for (int i = 0; i < len; i++) {
			if (delayedActions.get(i).timesUp(time)) {
				delayedActions.get(i).performAction();
				delayedActions.remove(i);
				i--;
			}
		}
	}
	/**
	 * Gets an {@link Entity} by its id.
	 * @param id the id
	 * @return {@link Entity}
	 *//*
	public Entity getEntity(final int id) {
		found = null;
		int len = entities.size();
		for (int i = 0; i < len; i++) {
			Entity entity = entities.get(i);
			if (entity.isAlive()
					&& entity.getID() == id) {
				found = entity;
				break;
			}
		}
		return found;
	}
	/**
	 * Gets the {@link GameClock}.
	 * @return {@link GameClock}
	 *//*
	public GameClock getGameClock() {
		return gameClock;
	}
	/**
	 * Gets the next entity id.
	 * @return int
	 *//*
	public int getNextId() {
		return ++entityId;
	}
	/**
	 * Reports the statistics about FPS/UPS.
	 * @param period the length in ns this iteration took
	 *//*
	private void reportStats(final long period) {
		// increment the times that frames have been counted
		gameClock.setFrameCount(gameClock.getFrameCount() + 1);
		long timeNow = System.nanoTime(); // get the current time
		// update the length of the stat recording interval in ms
		gameClock.setStatsInterval(gameClock.getStatsInterval() + period);

		// once the statsInterval get to 1 second, record the FPS data
		// in the array, then clear the statsInterval, to start over
		if (gameClock.getStatsInterval() >= GameClock.MAX_STATS_INTERVAL) {
			// get current time, and set the display to
			// show how long the game was played
			// convert ns -> seconds
			if (screen != null) {
				screen.setTimeSpentInGame(
						(int) ((timeNow - gameClock.getGameStartTime()
								- gameClock.getTimePaused())
								/ Engine.NANO_TO_SECOND)
						);
			}
			// update the timer if not paused
			// if( !gameClock.isPaused() )
			// currentScreen.setTimeSpent( currentScreen.getTimeSpentInGame() );
			// current time, minus time the last stats were taken
			long realElapsedTime = timeNow - gameClock.getPrevStatsTime();
			gameClock.setTotalElapsedTime(gameClock.getTotalElapsedTime()
					+ realElapsedTime);

			// if the interval went over 1000 ms, record it as the timingError
			// double timingError =
			// ((double)(realElapsedTime - gameClock.getStatsInterval()))
			// / gameClock.getStatsInterval() * 100.0;
			gameClock.setTotalFramesSkipped(gameClock.getTotalFramesSkipped()
					+ gameClock.getFramesSkipped());
			// calculate the latest FPS and UPS
			double actualFPS = 0.0;
			double actualUPS = 0.0;
			if (gameClock.getTotalElapsedTime() > 0) {
				// FPS is the num of frames displayed during
				// the elapsed time period
				actualFPS =
						(double) gameClock.getFrameCount()
								/ gameClock.getTotalElapsedTime()
								* Engine.NANO_TO_SECOND;
				// UPS is the num of frames (each frame counted for game update)
				// plus the num of frames skipped (each skip was a game update
				// as well) during the elapsed time period
				actualUPS =
						((double) gameClock.getFrameCount()
								+ gameClock.getTotalFramesSkipped())
								/ gameClock.getTotalElapsedTime()
								* Engine.NANO_TO_SECOND;
			}
			// store the latest FPS and UPS
			gameClock.setFpsStore((int) gameClock.getStatsCount()
					% GameClock.NUM_FPS, actualFPS);
			gameClock.setUpsStore((int) gameClock.getStatsCount()
					% GameClock.NUM_FPS, actualUPS);
			gameClock.setStatsCount(gameClock.getStatsCount() + 1);
			// total the stored FPS and UPS
			double totalFPS = 0.0;
			double totalUPS = 0.0;
			for (int i = 0; i < GameClock.NUM_FPS; i++) {
				totalFPS += gameClock.getFpsStore(i);
				totalUPS += gameClock.getUpsStore(i);
			} // end for
				// obtain the average FPS
			if (gameClock.getStatsCount() < GameClock.NUM_FPS) {
				gameClock.setAverageFPS(totalFPS / gameClock.getStatsCount());
				gameClock.setAverageUPS(totalUPS / gameClock.getStatsCount());
			} else {
				gameClock.setAverageFPS(totalFPS / GameClock.NUM_FPS);
				gameClock.setAverageUPS(totalUPS / GameClock.NUM_FPS);
			}
			// reset the frames skipped
			gameClock.setFramesSkipped(0);
			// set the previous stats time to current,
			// since we just recorded stats
			gameClock.setPrevStatsTime(timeNow);
			// reset the statsInterval
			gameClock.setStatsInterval(0L);
		}
		// logger.info("Average FPS:: " + gameClock.getAverageFPS());
		// logger.info("Average UPS:: " + gameClock.getAverageUPS());
	}*/
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void run() {
		// logger.info("run()");
		while (thread != null) {
			runThread();
		}
	}
	/** flag indicating the engine has started running. */
	private boolean engineRunning = false;
	/** Runs a thread for a game screen. */
	private void runThread() {
		// logger.info("runThread()");
		long beforeTime; // time before processing the gameUpdate and render
		long afterTime; // time after processing the gameUpdate and render
		long timeDiff; // time difference
		long sleepTime; // how long to sleep
		long overSleepTime = 0L; // how much time over the alloted loop
		// time the game processing took
		long excess = 0L; // the
		int delays = 0; // the number of times sleeping was delayed
		int skips = 0; // the number of frames skipped
		// due to excess processing time
		// initialize gameStartTime value before the loop
		Time.getInstance().init();
		//gameClock.setGameStartTime(System.nanoTime());
		// initialize the stats count
		//gameClock.setPrevStatsTime(gameClock.getGameStartTime());
		// initialize beforeTime value before the loop
		beforeTime = Time.getInstance().getGameTime(true);

		/*************************************************
		 * D E B U G *
		 *************************************************/

		//gameClock.setRunning(true);
		engineRunning = true;
		while (engineRunning && thread != null) {
			if (lastGarbage == 0L) {
				lastGarbage = System.nanoTime();
			}
			if (screen != null) {
				// mark the start of the frame
				Time.getInstance().startFrame();
				// update events
				InputEvent.getInstance().processActions();
				// update scripts
				Script.getInstance().update();
				// update animations
				Animation.getInstance().update();
				// update particles
				Particle.getInstance().update();
				// update game controller
				GameController.getInstance().update();
				// check interface for events
				Interface.getInstance().gameUpdate();
				// render the screen
				screen.display();
			}
			// of the last run iteration
			// get the time after game processing and check the difference
			afterTime = Time.getInstance().getGameTime(true);
			timeDiff = afterTime - beforeTime;
			sleepTime = Time.getInstance().getIdealLoopTime() 
					- timeDiff - overSleepTime;
			/*************************************************
			 * D E B U G *
			 *************************************************/
			PooledStringBuilder stringBuilder =
					StringBuilderPool.getInstance().getStringBuilder();
			stringBuilder.append("before time: ");
			stringBuilder.append(beforeTime);
			stringBuilder.append(" |after time: ");
			stringBuilder.append(afterTime);
			stringBuilder.append(" |timeDiff: ");
			stringBuilder.append(
					UtilityMethods.getInstance().convertNanoToMilli(timeDiff));
			stringBuilder.append("ms |sleep: ");
			stringBuilder.append(sleepTime);

			// if the game processing took under the
			// alloted loopTime, then sleep for a little bit
			if (sleepTime > 0) {
				try {
					// run garbage collection every 30 seconds
					if (UtilityMethods.getInstance().convertNanoToMilli(
							System.nanoTime()
							- lastGarbage) > 30000L) {
						lastGarbage = System.nanoTime();
						Object obj = new Object();
						WeakReference ref = new WeakReference<Object>(obj);
						obj = null;
						while (ref.get() != null) {
							System.gc();
						}
					}
					Thread.sleep((long) (sleepTime * DIV_NANO_MILLI));
				} catch (Exception ex) {
					JOGLErrorHandler.getInstance().fatalError(ex);
				}
				overSleepTime = Time.getInstance().getGameTime(true) 
						- afterTime - sleepTime;
			} else {
				// add up excess time the game processing took
				excess -= sleepTime;
				// clear out overSleepTime, since we're skipping sleep
				overSleepTime = 0L;
				// increment delays, and yield when it reaches the ceiling
				if (++delays >= GlobalConsts.NUM_DELAYS_PER_YIELD) {
					Thread.yield(); // yield to other processes
					delays = 0; // clear out all delays
				}
			}
			/*************************************************
			 * D E B U G *
			 *************************************************/
			stringBuilder.setLength(0);
			stringBuilder.append("excess sleep time: ");
			stringBuilder.append(excess);

			/*
			 * if the excess time spent processing is greater than one loopTime,
			 * then burn off that time doing updates for a maximum of
			 * MAX_FRAMES_SKIP times
			 */
			while (excess > Time.getInstance().getIdealLoopTime()
					&& skips < GlobalConsts.MAX_FRAMES_SKIP) {
				excess -= Time.getInstance().getIdealLoopTime();
				if (screen != null) {
					// check interface for events
					Interface.getInstance().gameUpdate();
					// gameUpdate(now); // update the game's state
					//if (!gameClock.isPaused()) {
					//	updateEntities(now);
					//}
				}
				/*************************************************
				 * D E B U G *
				 *************************************************/
				// logger.info("did a skip");

				skips++;
			} // end inner while
			//gameClock.setFramesSkipped(gameClock.getFramesSkipped() + skips);
			/*************************************************
			 * D E B U G *
			 *************************************************/
			stringBuilder.setLength(0);
			stringBuilder.append("frames skipped: ");
			stringBuilder.append(skips);
			StringBuilderPool.getInstance().returnObject(stringBuilder);
			
			// logger.info(buffer.toString());
			// one we reach maximum skips, clear out the variable
			if (skips >= GlobalConsts.MAX_FRAMES_SKIP) {
				skips = 0;
			}
			// get beforeTime again
			beforeTime = Time.getInstance().getGameTime(true); 
			// for next loop
			// send to reportStats the length in ns this iteration took
			//reportStats(beforeTime - loopStartTime);
			StringBuilderPool.getInstance().returnObject(stringBuilder);
		} // end while
			// printStats();
			// System.exit(0);
	}
	/**
	 * Sets the current {@link GameDrawable} being displayed.
	 * @param drawable the {@link GameDrawable}
	 */
	public void setScreen(final GameDrawable drawable) {
		screen = drawable;
	}
	/** Stops the game thread. */
	public void stop() {
		// logger.info("stop()");
		thread = null;
	}
	/**
	 * Updates all game entities.
	 * @param time the time in nanoseconds
	 *//*
	private void updateEntities(final long time) {
		int len = entities.size();
		for (int i = 0; i < len; i++) {
			if (entities.get(i).isAlive()) {
				// tell the game engine the entity has been updated
				gameEntityUpdate(entities.get(i), time);
				// see if this entity will auto-expire
				if (entities.get(i).getLifetime() > 0) {
					if (entities.get(i).lifetimeExpired()) {
						entities.get(i).setAlive(false);
					}
				}
			}
		}
	}
	*/
}
